<?php
/**
 * Shipping Agent for Zen Cart implementation.
 *
 * @package shipping_agent
 * @copyright Copyright 2009 Structured Solutions
 * @license http://www.codeplex.com/ShippingAgent/license Microsoft Public License (Ms-PL)
 * @version 3.2
 */

/**
 * Zen Cart currencies class.
 * Used here to format currency values.
 */
require_once("currencies.php");

/**
 * Zen Cart order class.
 */
require_once("order.php");

/**
 * SOAP classes from shippingagent.wsdl
 */
require_once("shippingagent_soap.php");

/**
 * The default value for ShippingAddress.Residential.
 * @var bool
 */
define("DEFAULT_RESIDENTIAL", false);

/**
 * PAID_ORDER_STATUS is used in buildPayments to determine if the order is
 * paid or not.
 * @var int
 */
define("PAID_ORDER_STATUS", 2);

/**
 * SHIPPED_ORDER_STATUS is used in two ways:
 * 
 * 1. In buildPackageResult, if the current order status >= SHIPPED_ORDER_STATUS,
 * then Package.Shipped = true.
 * 
 * 2. In buildUpdateResult, if the package is being shipped and the current order
 * status < SHIPPED_ORDER_STATUS, then the order status is set to SHIPPED_ORDER_STATUS.
 * @var int 
 */
define("SHIPPED_ORDER_STATUS", 3);

/**
 * shipping_agent_impl Class.
 * Implements the SOAP interface defined by shipping_agent.wsdl. 
 * @package shipping_agent
 * @subpackage classes
 * @author Structured Solutions
 */
class shippingagent_impl
{
	/**
	 * Return one Order.
	 * $parameters->orderId is the ID of the order to retrieve.
	 * @param stdClass $parameters
	 * @return GetOrderResponse
	 */
	public function GetOrder(stdClass $parameters)
	{
		if (!isset($parameters->orderId) || trim($parameters->orderId) == '')
			throw new SoapFault("Client", "Missing order ID.");

		try
		{
			$zcOrder = new zen_cart_order($parameters->orderId);

			$response = new GetOrderResponse();
			$response->GetOrderResult = $this->buildOrderResult($zcOrder);
			
			return $response;
		}
		catch (Exception $ex)
		{
			throw new SoapFault("Server", "Cannot retrieve order. " . $ex->getMessage());
		}
	}
	
	/**
	 * Return new orders.
	 * "New" in this implementation means orders with an order id greater than the
	 * value in pageKey.
	 * 
	 * $parameters->pageKey is the order id of the most recent order returned. If the
	 * value is null, this implementation will return orders placed today.
	 * 
	 * $parameters->maxCount is the maximum number of orders to return. If maxCount or 
	 * more orders are returned, Shipper will call this function again, passing in the
	 * pageKey from the previous GetNewOrdersResponse.
	 * @param stdClass $parameters
	 * @return GetNewOrdersResponse
	 */
	public function GetNewOrders(stdClass $parameters)
	{
    	global $db;
		
		try
		{
			$where = "";
			
			if (isset($parameters->pageKey) && $parameters->pageKey != "")
			{
				$where = " where orders_id >= '" . $parameters->pageKey . "'";
			}
			else
			{
				$where = " where date_purchased >= '" . date("Y-m-d 00:00:00") . "'";
			}
			
		    $orders = $db->Execute("
		    	select orders_id 
		    	from " . TABLE_ORDERS . 
		    	$where . " order by orders_id");
		    
		    if ($orders->RecordCount() <= 0) return null;

		    $response = new GetNewOrdersResponse();
		    $response->GetNewOrdersResult = new OrderResultsPage();
		    
		    while (!$orders->EOF)
		    {
		    	$orderId = $orders->fields["orders_id"];
		    	$zcOrder = new zen_cart_order($orderId);
		    	$response->GetNewOrdersResult->OrderResults[] = $this->BuildOrderResult($zcOrder);
		    	
		    	if (count($response->GetNewOrdersResult->OrderResults) >= $parameters->maxCount)
		    		break;
		    		
		    	$orders->MoveNext();
		    }
			
			$response->GetNewOrdersResult->Key = $orderId;
			return $response;
			
		}
		catch (Exception $ex)
		{
			throw new SoapFault("Server", "Cannot return new orders. " . $ex->getMessage());
		}
	}
	
	/**
	 * Return the current inventory level of each item in the order.
	 * $parameters->orderId is the ID of the order to retrieve.
	 * @param stdClass $parameters 
	 * @return GetOrderInventoryResponse
	 */
	public function GetOrderInventory(stdClass $parameters)
	{
		if (!isset($parameters->orderId) || trim($parameters->orderId) == '')
			throw new SoapFault("Client", "Missing order ID.");

		try
		{
			$zcOrder = new zen_cart_order($parameters->orderId);

			$response = new GetOrderInventoryResponse();
			$response->GetOrderInventoryResult = $this->buildOrderInventoryResult($zcOrder);
			
			return $response;
		}
		catch (Exception $ex)
		{
			throw new SoapFault("Server", "Cannot retrieve order inventory. " . $ex->getMessage());
		}
	}
	
	/**
	 * Return the orders that meet the criteria specified in the parameters.
	 * $parameters->startOrderNumber is the first order number to retrieve. If null, start at the first order.
	 * 
	 * $parameters->endOrderNumber is the last order number to retrieve. If null, end at the most recent order.
	 * 
	 * $parameters->startOrderDate is the beginning of the date range to search. If null, ignore the beginning date.
	 * 
	 * $parameters->endOrderDate is the end of the date range to search. If null, ignore the end date.
	 * 
	 * $parameters->pageKey is a key used by Shipping Agent to determine which page of the results to return. If null, return the first page.
	 * 
	 * $parameters->maxCount is the page size. If maxCount or more orders are returned, Shipper will call this function again with the pageKey that is returned in GetOrdersByRangeResponse.
	 *  
	 * @param stdClass $parameters
	 * @return GetOrdersByRangeResponse
	 */
	public function GetOrdersByRange(stdClass $parameters)
	{
    	global $db;
		
		try
		{
			$conditions = array();
			
			if (isset($parameters->startOrderNumber))
			{
				if (!settype($parameters->startOrderNumber, "int"))
					throw new SoapFault("Client", "Invalid starting order number.");
				$conditions[] = "orders_id >= '" . $parameters->startOrderNumber . "'";
			}
			
			if (isset($parameters->endOrderNumber))
			{
				if (!settype($parameters->endOrderNumber, "int"))
					throw new SoapFault("Client", "Invalid ending order number.");
				$conditions[] = "orders_id <= '" . $parameters->endOrderNumber . "'";
			}
			
			if (isset($parameters->startOrderDate))
			{
				$date = date_create($parameters->startOrderDate);
				if (!isset($date))
					throw new SoapFault("Client", "Invalid starting date.");
				$conditions[] = "date_purchased >= '" . $date->format("Y-m-d H:i:s") . "'";
			}
			
			if (isset($parameters->endOrderDate))
			{
				$date = date_create($parameters->endOrderDate);
				if (!isset($date))
					throw new SoapFault("Client", "Invalid ending date.");
				$conditions[] = "date_purchased <= '" . $date->format("Y-m-d H:i:s") . "'";
			}
						
			if (isset($parameters->pageKey) && $parameters->pageKey != "")
			{
				$where = " orders_id >= '" . $parameters->pageKey . "'";
			}
			
			if (count($conditions) > 0) 
				$where = " where " . implode(" and ", $conditions);
			else
				$where = ""; 
			
		    $orders = $db->Execute("
		    	select orders_id 
		    	from " . TABLE_ORDERS . 
		    	$where . " order by orders_id");
		    
		    if ($orders->RecordCount() <= 0) return null;

		    $response = new GetOrdersByRangeResponse();
		    $response->GetOrdersByRangeResult = new OrderResultsPage();
		    
		    while (!$orders->EOF)
		    {
		    	$orderId = $orders->fields["orders_id"];
		    	$zcOrder = new zen_cart_order($orderId);
		    	$response->GetOrdersByRangeResult->OrderResults[] = $this->BuildOrderResult($zcOrder, $orderId);
		    	
		    	if (count($response->GetNewOrdersResult->OrderResults) >= $parameters->maxCount)
		    		break;
		    	
	    		$orders->MoveNext();
		    }
			
			$response->GetOrdersByRangeResult->Key = $orderId;
			return $response;
		}
		catch (Exception $ex)
		{
			throw new SoapFault("Server", "Cannot retrieve orders by range. " . $ex->getMessage());
		}		
	}
	
	/**
	 * Return one package.
	 * $parameters->orderId is the ID of the order the package belongs to.
	 * 
	 * $parameters->packageId is the ID of the package. This parameter is ignored
	 * because Zen Cart only allows 1 shipment or packge per order. So the
	 * packageId==orderId.
	 * @param stdClass $parameters 
	 * @return GetPackageResponse
	 */
	public function GetPackage(stdClass $parameters)
	{
		if (!isset($parameters->orderId) || trim($parameters->orderId) == '')
			throw new SoapFault("Client", "Missing order ID.");
			
		try
		{
			$zcOrder = new zen_cart_order($parameters->orderId);
			if (!$zcOrder->valid)
				throw new Exception("Invalid order ID.");
		
			$response = new GetPackageResponse();
			$response->GetPackageResult = $this->buildPackageResult($zcOrder, $parameters->orderId);
			
			return $response;
		}
		catch (Exception $ex)
		{
			throw new SoapFault("Server", "Cannot retrieve package. ".$ex->getMessage());
		}
	}

	/**
	 * Return the complete list of tracking providers supported by this cart.
	 * This implementation returns null because Zen Cart does not have built-in tracking.
	 * @return null
	 */
	public function GetTrackingProviders()
	{
		return null;
	}

	/**
	 * Update the package with tracking information.
	 * @param stdClass $parameters
	 * @return UpdatePackageResponse
	 */
	public function UpdatePackage(stdClass $parameters)
	{
		if (!isset($parameters->update))
			return null;
			
		if (!isset($parameters->update->OrderId) || trim($parameters->update->OrderId) == '')
			throw new SoapFault("Client", "Missing order ID.");
			
		try
		{
			$response = new UpdatePackageResponse();
			$response->UpdatePackageResult = $this->buildUpdateResult($parameters->update);
			
			return $response;
		}
		catch (Exception $ex)
		{
			throw new SoapFault("Server", "Cannot update package. " . $ex->getMessage());
		}
	}
	
	/**
	 * Compare two addresses and return true if equal.
	 * @param stdClass $address
	 * @param array $zcAddress
	 */
	private function addressesAreEqual(stdClass $address, array $zcAddress)
	{
		if ($address->City != $zcAddress["city"]) return false;
		if ($address->Company != $zcAddress["company"]) return false;
		if ($address->Country != $zcAddress["country"]) return false;
		if (trim($address->FirstName . " " . $address->LastName) != trim($zcAddress["name"])) return false;
		if ($address->Line1 != $zcAddress["street_address"]) return false;
		if ($address->Line2 != $zcAddress["suburb"]) return false;
		if ($address->PostalCode != $zcAddress["postcode"]) return false;
		if ($address->Region != $zcAddress["state"]) return false;
		return true;
	}

	/**
	 * Build a shipping address from an order address array.
	 * Note that Shipper expects 2 letter country, state, and
	 * province codes.
	 * @param array $zcAddress
	 * @return ShippingAddress
	 */
	private function buildAddress(array $zcAddress)
	{
		global $db;
		
		$country = $db->Execute("
			select countries_id, countries_iso_code_2
			from " . TABLE_COUNTRIES . " 
			where countries_name = '" . $zcAddress["country"] . "'");
		
		$zone = $db->Execute("
			select zone_code
			from " . TABLE_ZONES . "
			where zone_country_id = '" . $country->fields["countries_id"] . "'
			  and zone_name = '" . $zcAddress["state"] . "'");
		
		$address = new Address();
		$address->City = $zcAddress["city"];
		$address->Company = $zcAddress["company"];
		if ($country->EOF)
			$address->Country = $zcAddress["country"];
		else
			$address->Country = $country->fields["countries_iso_code_2"];
		$address->FirstName = $this->getFirstName($zcAddress["name"]);
		$address->LastName = $this->getLastName($zcAddress["name"]);
		$address->Line1 = $zcAddress["street_address"];
		$address->Line2 = $zcAddress["suburb"];
		$address->PostalCode = $zcAddress["postcode"];
		if ($zone->EOF)
			$address->Region = $zcAddress["state"];
		else
			$address->Region = $zone->fields["zone_code"];
		$address->AddressId = $this->getAddressHash($address);
		return $address;
	}
	
	/**
	 * Build a list of orders charges.
	 * Discounts are included as order items. Gift Certificates are treated as payments.
	 * @param zen_cart_order $zcOrder
	 * @return array
	 */
	private function buildCharges(zen_cart_order $zcOrder)
	{
		global $db;
		
		$charges = array();
		foreach($zcOrder->totals as $zcTotal)
		{
			$amount = $this->getFloat($zcTotal["text"]);
			if ($amount == (float)0) continue;

			$namedAmount = new NamedAmount();
			$namedAmount->Amount = $amount;
			switch ($zcTotal["class"])
			{
				case "ot_cod_fee":
					$namedAmount->Name = "COD Fee";
					break;
				case "ot_coupon":
					$coupon = $db->Execute("
						select title
                        from " . TABLE_ORDERS_TOTAL . "
                        where orders_id = '" . $zcOrder->id . "'
                          and class = 'ot_coupon'");
					if (!$coupon->EOF)
						$description = $coupon->fields["title"];
					$description = str_replace("Discount Coupon", "", $description);
					$description = str_replace(":", "", $description);
					$namedAmount->Description = trim($description);
					$namedAmount->Name = "Discount Coupon";
					break;
				case "ot_group_pricing":
					$namedAmount->Name = "Group Discount";
					break;
				case "ot_gv":
					$namedAmount->Name = "Gift Certificates";
					break;
				case "ot_loworderfee":
					$namedAmount->Name = "Low Order Fee";
					break;
				case "ot_shipping":
					$namedAmount->Description = $zcOrder->info["shipping_method"];
					$namedAmount->Name = "Shipping";
					break;
				case "ot_tax":
					$namedAmount->Name = "Taxes";
					break;
			}
			$charges[] = $namedAmount;
		}
		return $charges;
	}
	
	/**
	 * Build a Customer from an order customer.
	 * @param array $zcCustomer
	 * @return Customer
	 */
	private function buildCustomer(array $zcCustomer)
	{
		$customer = new Customer();
		$customer->CustomerId = $zcCustomer["id"];
		$customer->CustomerUrl = $this->buildCustomerUrl("index.php", "main_page=account");
		$customer->Email = $zcCustomer["email_address"];
		$customer->FirstName = $this->getFirstName($zcCustomer["name"]);
		$customer->LastName = $this->getLastName($zcCustomer["name"]);
		$customer->MerchantUrl = $this->buildMerchantUrl("customers.php", "cID=".$zcCustomer["id"]);
		$customer->UserName = $zcCustomer["email_address"];
		return $customer;
	}
	
	/**
	 * Return the customer comments from an order.
	 * @param zen_cart_order $zcOrder
	 * @return string
	 */
	private function buildCustomerComments(zen_cart_order $zcOrder)
	{
	    global $db;
	    
	    $orders_comments = $db->Execute(
	    	"select comments 
	    	 from " . TABLE_ORDERS_STATUS_HISTORY . "
             where orders_id = '" . $zcOrder->id . "'
	         order by date_added
	         limit 1");
	    return $orders_comments->EOF ? "" : $orders_comments->fields['comments'];
	}

	/**
	 * Build a URL to the customer's view of the page.
	 * @param string $page
	 * @param string $query
	 * @return string
	 */
	private function buildCustomerUrl($page, $query)
	{
		if (ENABLE_SSL_CATALOG == "true")
			$webstore_address = HTTPS_SERVER.DIR_WS_HTTPS_CATALOG;
		else
			$webstore_address = HTTP_SERVER.DIR_WS_CATALOG;
		$url = $webstore_address.$page;
		if (isset($query) && $query != "")
			$url = $url . "?" . $query;
		return $url;
	}
	
	/**
	 * Build a list of Items from the order.
	 * @param zen_cart_order $zcOrder
	 * @param boolean $includeVirtualProducts
	 * @return array
	 */
	private function buildItems(zen_cart_order $zcOrder, $includeVirtualProducts = true)
	{
		global $db;
		
		$items = array();
		$orderBy = 0;
		
		foreach ($zcOrder->products as $zcItem)
		{
			$descriptions = array();
			foreach ($zcItem["attributes"] as $attribute)
			{
				$descriptions[] = $attribute["option"].": ".$attribute["value"];
			}
			
			// Used for the current inventory count.
			$product = $db->Execute("
				select products_quantity, products_virtual
				from " . TABLE_PRODUCTS . " 
				where products_id = '". $zcItem["id"] . "'");
			
			$item = new Item();
			$item->Code = $zcItem["model"];
			$item->Description = implode(", ", $descriptions);
			$item->Inventory = $product->EOF ? (float) 0 : (float) $product->fields["products_quantity"];
			$item->ItemId = $zcItem["id"];
			$item->LineTotal = (float) ($zcItem["qty"] * (float) $zcItem["final_price"]);
			$item->Name = $zcItem["name"];
			$item->OrderBy = $orderBy++;
			$item->Price = (float) $zcItem["price"];
			$item->Quantity = (float) $zcItem["qty"];
			$item->Shippable = $product->EOF ? true : !((bool) $product->fields["products_virtual"]);
			if ($item->Shippable || $includeVirtualProducts)
				$items[] = $item;
		}
		
		return $items;
	}
		
	/**
	 * Build a URL to the merchant's view of the page.
	 * @param string $page
	 * @param string $query
	 * @return string
	 */
	private function buildMerchantUrl($page, $query)
	{
		if (!defined("ENABLE_SSL_ADMIN")) return null;
		
		if (ENABLE_SSL_ADMIN == "true")
		{
			if (!defined("DIR_WS_HTTPS_ADMIN")) return null;
			$admin_address = HTTPS_SERVER . DIR_WS_HTTPS_ADMIN;
		}
		else
		{
			if (!defined("DIR_WS_ADMIN")) return null;
			$admin_address = HTTP_SERVER . DIR_WS_ADMIN;
		}
		$url = $admin_address.$page;
		if (isset($query) && $query != "")
			$url = $url . "?" . $query;
		return $url;
	}
	
	/**
	 * Build a list of current inventory counts for all items in an order.
	 * @param zen_cart_order $zcOrder
	 * @return array
	 */
	private function buildOrderInventoryResult(zen_cart_order $zcOrder)
	{
		global $db;
		
		$inventoryResult = new InventoryResult();
		
		try
		{
			if (!$zcOrder->valid)
				throw new Exception("Invalid order ID '" . $zcOrder->id . "'.");
			
			foreach ($zcOrder->products as $zcItem)
			{
				$product = $db->Execute("
					select products_quantity from products 
					where products_id = '". $zcItem["id"] . "'");
				
				$itemCount = new ItemCount();
				$itemCount->ItemId = $zcItem["id"];
				if (!$product->EOF)
					$itemCount->Count = $product->fields["products_quantity"];
				$inventoryResult->ItemCounts[] = $itemCount;
			}
			
			$inventoryResult->Success = true;
		}
		catch (Exception $ex)
		{
			$inventoryResult->Success = false;
			$inventoryResult->Message = $ex->getMessage();
		}
		
		return $inventoryResult;
	}
	
	/**
	 * Build an OrderResult from a Zen Cart order.
	 * @param zen_cart_order $zcOrder
	 * @return OrderResult
	 */
	private function buildOrderResult(zen_cart_order $zcOrder)
	{
		$orderResult = new OrderResult();
		$orderResult->Order = new ShippingAgentOrder();
		$orderResult->Order->OrderId = $zcOrder->id;

		try
		{
			if (!$zcOrder->valid)
				throw new Exception("Invalid order ID '" . $zcOrder->id . "'.");
				
			$orderResult->Order->BillingAddress = $this->buildAddress($zcOrder->billing);
			$orderResult->Order->Charges = $this->buildCharges($zcOrder);
			$orderResult->Order->Customer = $this->buildCustomer($zcOrder->customer);
			$orderResult->Order->CustomerUrl = $this->buildCustomerUrl("index.php", "main_page=account_history_info&order_id=" . $zcOrder->id);
			$orderResult->Order->Items = $this->buildItems($zcOrder, true);
			$orderResult->Order->MerchantUrl = $this->buildMerchantUrl("orders.php", "&oID=" . $zcOrder->id);
			$orderResult->Order->OrderDate = date_format(date_create($zcOrder->info["date_purchased"]), "c");
			$orderResult->Order->OrderNumber = $zcOrder->id;
			$orderResult->Order->OrderStatus = $zcOrder->info["orders_status"];
			$orderResult->Order->OrderTotal = (float) $zcOrder->info["total"];
			$orderResult->Order->Packages = $this->buildPackageResults($zcOrder);
			$orderResult->Order->Payments = $this->buildPayments($zcOrder, $orderId);
			$orderResult->Success = true;
		}
		catch (Exception $ex)
		{
			$orderResult->Success = false;
			$orderResult->Message = $ex->getMessage();
		}
		
		return $orderResult;
	}
	
	/**
	 * Build a PackageResult from a Zen Cart order.
	 * Zen Cart only allows for one shipment per order.
	 * @param zen_cart_order $zcOrder
	 * @return PackageResult
	 */
	private function buildPackageResult(zen_cart_order $zcOrder)
	{
		global $db;

		$packageResult = new PackageResult();
		$packageResult->Package = new Package();
		$packageResult->Package->OrderId = $zcOrder->id;
		$packageResult->Package->PackageId = $zcOrder->id;
		
		try
		{
			$order_status = $db->Execute("
				 select orders_status 
				 from " . TABLE_ORDERS . " 
				 where orders_id = '" . $zcOrder->id . "'");
			
			$packageResult->Package->Charges = $this->buildCharges($zcOrder);
			$packageResult->Package->CustomerUrl = $this->buildCustomerUrl("index.php", "main_page=account_history_info&order_id=" . $zcOrder->id);
			$packageResult->Package->Destination = $this->buildShippingAddress($zcOrder->delivery);
			$packageResult->Package->Instructions = $this->buildCustomerComments($zcOrder);
			$packageResult->Package->Items = $this->buildItems($zcOrder, false);
			$packageResult->Package->MerchantUrl = $this->buildMerchantUrl("orders.php", "&oID=" . $zcOrder->id);
			$packageResult->Package->Shipped = $order_status->fields["orders_status"] >= SHIPPED_ORDER_STATUS;
			$packageResult->Package->RateOptionName = $zcOrder->info["shipping_method"];
			$packageResult->Success = true;
		}
		catch (Exception $ex)
		{
			$packageResult->Success = false;
			$packageResult->Message = $ex->getMessage();
		}
		
		return $packageResult;
	}
	
	/**
	 * Build a list of PackageResults from a Zen Cart order.
	 * Zen Cart only supports 1 shipment per order.
	 * @param zen_cart_order $zcOrder
	 * @return array
	 */
	private function buildPackageResults(zen_cart_order $zcOrder)
	{
		$packages = array();
		$package = $this->buildPackageResult($zcOrder);
		if (count($package->Package->Items) > 0)
			$packages[] = $package; 
		return $packages;
	}

	/**
	 * Build a list of order payments.
	 * @param zen_cart_order $zcOrder
	 * @return array]
	 */
	private function buildPayments(zen_cart_order $zcOrder)
	{
		if ($zcOrder->status < PAID_ORDER_STATUS)
			return null;
		
		$payments = array();
		
		$total = $this->getFloat($zcOrder->info["total"]);
		if ($total != 0)
		{
			$namedAmount = new NamedAmount();
			$namedAmount->Amount = $total; 
			$namedAmount->Description = $zcOrder->info["cc_number"];
			$namedAmount->Name = $zcOrder->info["payment_method"];
			$payments[] = $namedAmount;
		}

		return $payments;
	}

	/**
	 * Build a shipping address from an order address array.
	 * Note that Shipper expects 2 letter country, state, and
	 * province codes.
	 * @param array $zcAddress
	 * @return ShippingAddress
	 */
	private function buildShippingAddress($zcAddress)
	{
		global $db;
		
		$country = $db->Execute("
			select countries_id, countries_iso_code_2
			from " . TABLE_COUNTRIES . " 
			where countries_name = '" . $zcAddress["country"] . "'");
		
		$zone = $db->Execute("
			select zone_code
			from " . TABLE_ZONES . "
			where zone_country_id = '" . $country->fields["countries_id"] . "'
			  and zone_name = '" . $zcAddress["state"] . "'");
		
		$address = new ShippingAddress();
		$address->City = $zcAddress["city"];
		$address->Company = $zcAddress["company"];
		if ($country->EOF)
			$address->Country = $zcAddress["country"];
		else
			$address->Country = $country->fields["countries_iso_code_2"];
		$address->FirstName = $this->getFirstName($zcAddress["name"]);
		$address->LastName = $this->getLastName($zcAddress["name"]);
		$address->Line1 = $zcAddress["street_address"];
		$address->Line2 = $zcAddress["suburb"];
		$address->PostalCode = $zcAddress["postcode"];
		if ($zone->EOF)
			$address->Region = $zcAddress["state"];
		else
			$address->Region = $zone->fields["zone_code"];
		$address->Residential = DEFAULT_RESIDENTIAL;
		$address->AddressId = $this->getAddressHash($address);
		return $address;
	}
	
	/**
	 * Update the package with shipping information from Shipper.
	 * @param stdClass $update
	 * @return UpdateResult
	 */
	private function buildUpdateResult(stdClass $update)
	{
		global $db;
		
		$notes = array();

		$updateResult = new UpdateResult();
		try
		{
			$zcOrder = new zen_cart_order($update->OrderId);
			if (!$zcOrder->valid)
				throw new Exception("Invalid order ID.");
			
			// Shipper can correct the shipping address (e.g. fix typos and find
			// the ZIP+4 code). If the destination address is included in the update
			// this code will update the order address. You may want to update the
			// customers address book as well. 
			if (isset($update->Destination) && !$this->addressesAreEqual($update->Destination, $zcOrder->delivery))
			{
				$db->Execute("
					update " . TABLE_ORDERS . " set
				    delivery_name = '" . zen_db_input($update->Destination->FirstName . " " . $update->Destination->LastName) . "', 
				    delivery_company = '" . zen_db_input($update->Destination->Company) . "',
				    delivery_street_address = '" . zen_db_input($update->Destination->Line1) . "',
				    delivery_suburb = '" . zen_db_input($update->Destination->Line2) . "',
				    delivery_city = '" . zen_db_input($update->Destination->City) . "',
				    delivery_postcode = '" . zen_db_input($update->Destination->PostalCode) . "',
				    delivery_state = '" . zen_db_input($update->Destination->Region) . "',
				    delivery_country = '" . zen_db_input($update->Destination->Country) . "',
				    last_modified = now()
				    where orders_id = '" . (int)$update->OrderId . "'");
				
				$notes[] = "Updated order shipping address.";
			}
			
			// If Shipper sends a ship date, that means the package was shipped.
			// This code changes the order status to Delivered, and records the
			// ship date in an order note.
			if (isset($update->ShipDate))
			{
				if ($zcOrder->status < SHIPPED_ORDER_STATUS)
				{
					$zcOrder->status = SHIPPED_ORDER_STATUS;
					$db->Execute("
						update " . TABLE_ORDERS . "
		                set orders_status = '" . $zcOrder->status . "', last_modified = now()
		                where orders_id = '" . (int)$update->OrderId . "'");
					
					$statusName = $db->Execute("
						select orders_status_name
						from " . TABLE_ORDERS_STATUS . "
						where orders_status_id = '" . SHIPPED_ORDER_STATUS . "'
						  and language_id = '". (int)$_SESSION['languages_id'] . "'");
					$notes[] = "Changed order status to '" . $statusName->fields["orders_status_name"] . "'.";
				}
				
				$shipDate = date_create($update->ShipDate)->format("Y-m-d H:i:s");
				$notes[] = "Package was shipped on " . $shipDate . ".";
			}
			
			// This code will record the shipping cost in an order note. If you 
			// have extended the cart to store the shipping cost, this is the place
			// to record it.
			if (isset($update->ShippingCost))
			{
	  			$currencies = new currencies();
	  			// Shipping cost is always in USD.
				$cost = $currencies->format($update->ShippingCost, false, "USD", $update->ShippingCost);
				$notes[] = "Shipping cost was " . $cost . ".";
			}
			
			// This code will record the tracking numbers in an order note. If you 
			// have extended the cart to store tracking numbers, this is the place
			// to record them.
			if (isset($update->TrackingNumbers))
			{
				if (count($update->TrackingNumbers) == 1)
					$notes[] = "Tracking number was " . $update->TrackingNumbers . ".";
				else if (count($update->TrackingNumbers) > 1)
					$notes[] = "Tracking numbers were " . implode(", ", $update->TrackingNumbers) . ".";
			}
			
			// Update the order notes to record the changes.
			if (count($notes) > 0)
			{
	          	$db->Execute("insert into " . TABLE_ORDERS_STATUS_HISTORY . "
	                      	  (orders_id, orders_status_id, date_added, customer_notified, comments)
	                          values ('" . $update->OrderId . "',
	                          '" . zen_db_input($zcOrder->status) . "',
	                          now(),
	                          '0',
	                          '" . zen_db_input(implode(" ", $notes))  . "')");
			}
	
			// Return a fresh copy of the order, along with a copy of the notes.
			$zcOrder->query($update->UpdateId);
			$updateResult->OrderResult = $this->buildOrderResult($zcOrder);
			if (count($notes) > 0)
				$updateResult->Message = implode(" ", $notes);
			else
				$updateResult->Message = "No change.";
			$updateResult->Success = true;
		}
		catch (Exception $ex)
		{
			$updateResult->Message = $ex->getMessage();
			$updateResult->Success = false;
		}
		
		return $updateResult;
	}
	
	/**
	 * Create a unique ID for each unique Address.
	 * @param Address $address
	 * @return string
	 */
	private function getAddressHash(Address $address)
	{
		$val = "";
		if (isset($address->City)) $val .= $address->City;
		if (isset($address->Company)) $val .= $address->Company;
		if (isset($address->Country)) $val .= $address->Country;
		if (isset($address->FirstName)) $val .= $address->FirstName;
		if (isset($address->LastName)) $val .= $address->LastName;
		if (isset($address->Line1)) $val .= $address->Line1;
		if (isset($address->Line2)) $val .= $address->Line2;
		if (isset($address->PostalCode)) $val .= $address->PostalCode;
		if (isset($address->Region)) $val .= $address->Region;
		return md5($val);
	}
	
	private function getFirstName($name)
	{
		if (!isset($name) || trim($name) == "")
			return "";
			
		$names = explode(" ", trim($name));
		if (count($names) <= 1)
			return "";
			
		array_pop($names);
		return implode(" ", $names);
	}
	
	private function getFloat($text)
	{
		$nocommas = str_replace(",", "", $text);
		$amount = str_replace("$", "", $nocommas);
		return (float) $amount;
	}
	
	private function getLastName($name)
	{
		if (!isset($name) || trim($name) == "")
			return "";
			
		$names = explode(" ", trim($name));
		if (count($names) == 0)
			return "";
			
		return array_pop($names);
	}
}

/**
 * Extends Zen Cart order to add access to the id and status code.
 * @author Structured Solutions
 */
class zen_cart_order extends order
{
	var $id, $status, $valid;
	
	function zen_cart_order($order_id = '')
	{
		global $db;
		
		$this->id = $order_id;
		$this->order($order_id);
		if (isset($this->info, $this->info["orders_status"]))
	    {
			$order_status = $db->Execute("
				 select orders_status 
				 from " . TABLE_ORDERS . " 
				 where orders_id = '" . $order_id . "'");
			$this->status = (int)$order_status->fields["orders_status"];
			$this->valid = true;
        }
        else
        {
        	// This is either a cart (not an order) or invalid order id
        	$this->valid = false;
        }
	}
}
?>